
package application.smartcard;

import java.math.BigInteger;
import javax.smartcardio.ATR;
import javax.smartcardio.Card;
import javax.smartcardio.CardException;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;



/**
 * 
 * @author M. Cardinaux <matthieu.cardinaux at gmail.com>
 */
public class SmartCard {
    
    public static final int CLA_RU = 0x80;
    public static final int CLA_V = 0x00;
    public static final int INS_R = 0xBE;
    public static final int INS_U = 0xDE;
    public static final int INS_V = 0x20;
    public static final int P1 = 0x00;
    
    public static final int PARAM_PIN0 = 0x07;
    public static final int PARAM_PIN1 = 0x39;
    public static final int PARAM_PIN2 = 0x3B;
    public static final int PARAM_USER_EMUL = 0x3A;
    
    public static final int SUCCESS_CODE = 0x9000;
    
    public static final int PIN_SIZE = 0x04;
    public static final int KEY_SIZE = 0x04;
    public static final int OTP_SIZE = 0x04;
    public static final int SALT_SIZE = 0x08;
    
    
    public static final int ADDRESS_ACA = 0x05;
    
    public static final int ADDRESS_USERAREA1 = 0x10;
    
    public static final int ADDRESS_CTC2 = 0x20;
    public static final int ADDRESS_BAL2_A1 = 0x24;
    public static final int ADDRESS_BAL2_A2 = 0x26;
    
    public static final int ADDRESS_USERAREA2 = 0x28;
    
    public static final int ADDRESS_CSC1 = 0x38;
    public static final int ADDRESS_CRC2 = 0x3B;
    
    public static final int ADDRESS_CSC2 = 0x3A;
    
    public static final byte[] PIN0_DEFAULT = {(byte) 0xAA, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA};
    public static final byte[] PIN1_DEFAULT = {(byte) 0x11, (byte) 0x11, (byte) 0x11, (byte) 0x11};
    public static final byte[] PIN2_DEFAULT = {(byte) 0x22, (byte) 0x22, (byte) 0x22, (byte) 0x22};
    public static final byte[] NULL_WORD = {(byte) 0, (byte) 0, (byte) 0, (byte) 0};
    public static final byte[] TWO_NULL_WORD = {(byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0};
    public static final byte[] ACA_APP = {(byte) 0xEA, (byte) 0x00, (byte) 0x00, (byte) 0x00};
     
    private final Card card;

    
    
    public SmartCard(Card card){
        this.card = card;
    }
    
    public ATR getATR() {
        return card.getATR();
    }


    
    public ResponseAPDU read(int address, int le) throws CardException{
        CommandAPDU cmd = new CommandAPDU(CLA_RU, INS_R, P1, address, le);
        return card.getBasicChannel().transmit(cmd);
    }
    
    public ResponseAPDU update(int address, int lc, byte[] data) throws CardException{
        CommandAPDU cmd = new CommandAPDU(CLA_RU, INS_U, P1, address, reverseWords(data), lc);
        return card.getBasicChannel().transmit(cmd);
    }
    
    public ResponseAPDU verify(int param, byte[] data) throws CardException{
        CommandAPDU cmd = new CommandAPDU(CLA_V, INS_V, P1, param, reverseWords(data));
        return card.getBasicChannel().transmit(cmd);
    }
    
    public ResponseAPDU readChipInfo() throws CardException {
        CommandAPDU cmd = new CommandAPDU(0x80, 0xC0, 0x00, 0x00, 0x09);
        return card.getBasicChannel().transmit(cmd);
    }
    
    public byte[] getUidBytes() throws CardException {
        return readChipInfo().getData();
    }
    
    public boolean hasUid(byte[] uid) throws CardException {
        ResponseAPDU rsp = readChipInfo();
        return (hasApduCommandSucceeded(rsp) && compareByteArray(uid,rsp.getData()));
    }
    
    public ResponseAPDU readAca() throws CardException{
        return read(ADDRESS_ACA,0x04);
    }
    
    public ResponseAPDU updateAca(byte[] aca) throws CardException{
        return update(ADDRESS_ACA, 0x04, aca);
    }
    
    public ResponseAPDU emulateUserMode() throws CardException{
        return verify(PARAM_USER_EMUL,new byte[4]);
    }
    
    public ResponseAPDU presentPin0(byte[] pin0) throws CardException{
        return verify(PARAM_PIN0,pin0);
    }
    
    public ResponseAPDU presentPin1(byte[] pin1) throws CardException {
        return verify(PARAM_PIN1,pin1);
    }

    public ResponseAPDU presentPin2(byte[] pin2) throws CardException {
        return verify(PARAM_PIN2,pin2);
    }
    
    public ResponseAPDU updatePin1(byte[] pin1) throws CardException {
        return update(SmartCard.ADDRESS_CSC1, 0x04, pin1);
    }

    public ResponseAPDU updatePin2(byte[] pin2) throws CardException {
        return update(SmartCard.ADDRESS_CSC2, 0x04, pin2);
    }
    
    public ResponseAPDU updateKey(byte[] key) throws CardException {
        return update(ADDRESS_USERAREA1, KEY_SIZE, key);
    }

    public byte[] getKeyBytes() throws CardException, CardReadException {
        ResponseAPDU rsp = read(ADDRESS_USERAREA1,KEY_SIZE);
        if(!hasApduCommandSucceeded(rsp)) throw new CardReadException();
        return reverseWords(rsp.getData());
    }

    public byte[] getCtc2Bytes() throws CardReadException, CardException {
        ResponseAPDU rsp = read(ADDRESS_CTC2,0x04);
        if(!hasApduCommandSucceeded(rsp)) throw new CardReadException();
        return reverseWords(rsp.getData());
    }
    
    

    public byte[] getOtpBytes() throws CardReadException, CardException {
        ResponseAPDU rsp = read(ADDRESS_BAL2_A1,OTP_SIZE);
        if(!hasApduCommandSucceeded(rsp)) throw new CardReadException();
        return reverseWords(rsp.getData());    
    }

    public ResponseAPDU updateOtp(byte[] otp) throws CardException, CardWriteException {
        ResponseAPDU rsp = update(ADDRESS_BAL2_A1,OTP_SIZE,otp);
        if(!hasApduCommandSucceeded(rsp)){
            return update(ADDRESS_BAL2_A2,OTP_SIZE,NULL_WORD);
        }
        return rsp;
    }

    public byte[] getSalt2Bytes() throws CardReadException, CardException {
        ResponseAPDU rsp = read(ADDRESS_USERAREA2,SALT_SIZE);
        if(!hasApduCommandSucceeded(rsp)) throw new CardReadException();
        return reverseWords(rsp.getData());       
    }
    
    public ResponseAPDU updateSalt2(byte[] salt) throws CardException{
        return update(ADDRESS_USERAREA2,SALT_SIZE,salt);
    }
    
    public static boolean hasApduCommandSucceeded(ResponseAPDU rsp){
        return rsp.getSW() == SUCCESS_CODE;
    }
    
    public static String apduToString(ResponseAPDU rsp){
        StringBuilder sb = new StringBuilder();
        if(rsp.getData().length != 0) sb.append(toHex(reverseWords(rsp.getData()))).append(" ");
        sb.append("[").append(Integer.toHexString(rsp.getSW())).append("]");
        return sb.toString();
    }
    
    public static String toHex(byte[] bytes) {
        BigInteger bi = new BigInteger(1, bytes);
        return String.format("%0" + (bytes.length << 1) + "X", bi);
    }
    
    public static String toBin(byte b){
        return String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0');
    }
    
    private static byte[] reverseWords(byte[] src){
        byte[] dst = new byte[src.length];
        for(int i=0;i<src.length;i+=4){
            for(int j=0;j<4;j++){
                dst[i+j]=src[i+3-j];
            }
        }
        return dst;
    }

    private static int intFromByteArray(byte[] b) {
        return  b[3] & 0xFF | (b[2] >> 1) & 0xFF | (b[1] >> 2) & 0xFF | (b[0] >> 3) & 0xFF;
    }

    private boolean compareByteArray(byte[] uid, byte[] data) {
        if(uid.length != data.length) return false;
        for(int i=0;i<data.length;i++){
            if(uid[i]!=data[i]) return false;
        }
        return true;
    }
    
    public void resetToDefault() throws CardException, CardWriteException{
        presentPin0(PIN0_DEFAULT);
        updateAca(NULL_WORD);
        updatePin1(PIN1_DEFAULT);
        updatePin2(PIN2_DEFAULT);
        
        updateKey(NULL_WORD);
        updateSalt2(TWO_NULL_WORD);
        updateOtp(NULL_WORD);
        
        update(ADDRESS_CTC2, 0x04, NULL_WORD);
    }
    
}
